import commonjs, { RollupCommonJSOptions } from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import typescript, { CustomTransformerFactories } from '@rollup/plugin-typescript'
import hot from '@xyh19/hot-module-cjs/dist/rollup-plugin'
import assets, { defaultInclude, UserOptions as AssetsOptions } from '@xyh19/rollup-plugin-assets'
import definePlugin from '@xyh19/rollup-plugin-define'
import externalsPlugin from '@xyh19/rollup-plugin-node-externals'
import { merge, once } from 'lodash-es'
import path from 'path'
import rollup from 'rollup'
import analyzer from 'rollup-plugin-analyzer'
import esbuild, { Options as EsbuildOptions } from 'rollup-plugin-esbuild'
import vite, { normalizePath } from 'vite'
import { CWD, DEFAULT_RENDERER_DIR, JS_TYPES_RE } from './constants'
import { replaceDir } from './plugin'
import { log, relativePath, safeRequireResolve, toArray } from './utils'

export interface UserOptions {
  input?: {
    /**
     * app入口
     * @default (__dirname+'/../lib/app-entry.js')
     */
    appEntry?: string
    /**
     * 其他入口(preload等)
     */
    entries?: string | string[]
    srcRoot?: string
  }
  output?: {
    /**
     * 输出目录
     * @default 'dist'
     */
    dir?: string
    /**
     * renderer 输出目录，相对于 output.dir
     * @default 'renderer'
     */
    rendererDir?: string
    /**
     * Whether to Object.freeze() namespace import objects (i.e. import * as namespaceImportObject from...) that are accessed dynamically.
     * @default true
     */
    freeze?: boolean
    /**
     * 要在捆绑包中添加/附加的字符串。您还可以提供一个函数，该函数返回解析为字符串的承诺，以异步生成它（注意：横幅和页脚选项不会破坏sourcemaps）。
     */
    banner?: string | (() => string | Promise<string>)
    footer?: string | (() => string | Promise<string>)
    intro?: string | (() => string | Promise<string>)
    outro?: string | (() => string | Promise<string>)
    /**
     * 允许创建自定义共享公共块。当使用对象形式时，每个属性代表一个包含列出的模块及其所有依赖项的块，如果它们是模块图的一部分，除非它们已经在另一个手动块中。块的名称将由属性键确定。
     * @example ```
     *  manualChunks: {
     *    lodash: ['lodash']
     *  }
     * ```
     */
    manualChunks?: rollup.ManualChunksOption
    /** Maps external module IDs to paths. External ids are ids that cannot be resolved or ids explicitly provided by the external option. Paths supplied by output.paths will be used in the generated bundle instead of the module ID, allowing you to, for example, load dependencies from a CDN */
    paths?: rollup.OptionsPaths
    sourcemap?: boolean | 'inline' | 'hidden'
    sourcemapExcludeSources?: boolean
    sourcemapFile?: string
    sourcemapPathTransform?: rollup.SourcemapPathTransformOption
    validate?: boolean
  }
  root?: string
  /**
   * rollup watch
   */
  watch?: rollup.WatcherOptions | false
  /**
   * 是否强制重启
   * @default false
   */
  forceRestart?: boolean | (() => boolean | Promise<boolean>)
  /**
   * ts配置文件
   * @default (process.cwd()+'tsconfig.json')
   */
  tsconfig?: false | string
  tsTransformers?: CustomTransformerFactories
  /**
   * 定义全局常量替换方式。其中每项在开发环境下会被定义在全局，而在构建时被静态替换。
   * @see https://cn.vitejs.dev/config/#define
   */
  define?: Record<string, string>
  build?: import('electron-builder').Configuration
  /**
   * rollup插件
   */
  plugins?: (
    | (rollup.Plugin & {
        enforce?: 'pre' | 'post'
      })
    | null
    | false
    | undefined
  )[]
  /**
   * 外部模块
   */
  external?: (string | RegExp)[]
  /**
   * 外部模块
   */
  noExternal?: (string | RegExp)[]
  cache?: false | rollup.RollupCache
  commonjsOptions?: RollupCommonJSOptions
  esbuild?: false | EsbuildOptions
  esbuildOptions?: EsbuildOptions
  treeshake?: rollup.TreeshakingOptions
  /**
   * @deprecated
   * 指定额外的 picomatch 模式 作为静态资源处理。
   */
  assetsInclude?: string | RegExp | (string | RegExp)[]
  assetsOptions?: AssetsOptions
}

export const getDirConfig = once((options: UserOptions) => {
  const _dir = options.output?.dir ?? 'dist'
  const _appEntry = options.input?.appEntry
  const _srcRoot = options.input?.srcRoot

  const root = path.join(CWD, options.root || './')
  const mainDir = path.resolve(root, _dir)
  const rendererDir = path.resolve(root, _dir, options.output?.dir || DEFAULT_RENDERER_DIR)

  const appEntry = (_appEntry && path.resolve(root, _appEntry)) || path.join(__dirname, '../lib/app-entry.js')
  const mainSourceRoot = (_srcRoot && path.resolve(root, _srcRoot)) || (_appEntry && path.dirname(appEntry))

  const _mainSourceRoot = mainSourceRoot || path.dirname(appEntry)

  return {
    root: path.normalize(root),
    mainDir: path.normalize(mainDir),
    rendererDir: path.normalize(rendererDir),
    appEntry: path.normalize(appEntry),
    mainSourceRoot: mainSourceRoot ? path.normalize(mainSourceRoot) : undefined,
    resolveEntryName: (entry: string) => {
      entry = path.resolve(root, entry)
      return path.relative(_mainSourceRoot, entry).replace(JS_TYPES_RE, '')
    }
  }
})

export const fixViteConfig = (options: UserOptions, config: vite.UserConfig, { mode }: vite.ConfigEnv) => {
  const { rendererDir, mainDir } = getDirConfig(options)

  config.build = config.build ?? {}
  config.build.outDir = rendererDir

  const serverOptions = config.server ?? (config.server = {})
  const watchOpions = serverOptions.watch ?? (serverOptions.watch = {})
  const watchIgnored = (watchOpions.ignored = toArray(watchOpions.ignored))
  watchIgnored.push(mainDir)

  const define = config.define ?? (config.define = Object.create(null))
  if (!('__DEV__' in define)) {
    define.__DEV__ = String(mode === 'development')
  }
  if (!('__PROD__' in define)) {
    define.__PROD__ = String(mode === 'production')
  }
}

export const generateElectronRollupOptions = (
  options: UserOptions,
  { mode, command }: vite.ConfigEnv
): rollup.RollupWatchOptions => {
  const { root, mainDir, rendererDir, mainSourceRoot, appEntry, resolveEntryName } = getDirConfig(options)
  const isBuild = command === 'build'

  const entryItem = (entry: string, name?: string) => {
    entry = path.resolve(root, entry)
    return [name ?? resolveEntryName(entry), normalizePath(relativePath(entry))] as const
  }

  const plugins = (options.plugins || []).flat()
  const prePlugins = plugins.filter((plugin: any) => plugin && plugin.enforce === 'pre')
  const postPlugins = plugins.filter((plugin: any) => plugin && plugin.enforce !== 'pre')

  const sourcemap = !!(options.output?.sourcemap ?? !isBuild)
  let esbuildOptions =
    (options.esbuild && options.esbuildOptions
      ? merge({}, options.esbuild, options.esbuildOptions)
      : options.esbuild ?? options.esbuildOptions) || {}
  esbuildOptions = {
    tsconfig: options.tsconfig,
    minify: isBuild,
    sourceMap: true,
    target: 'es2019',
    keepNames: !isBuild,
    legalComments: isBuild ? 'none' : undefined,
    ...esbuildOptions,
    loaders: { '.json': 'json', ...esbuildOptions.loaders },
    experimentalBundling: false
  }

  const watchOptions = options.watch ?? {}
  if (watchOptions) {
    const exclude = (watchOptions.exclude = toArray(watchOptions.exclude))
    exclude.push(mainDir)
  }

  const hasReflectMetadata = !!safeRequireResolve('reflect-metadata')

  return {
    context: 'global',
    watch: options.watch,
    cache: options.cache,
    treeshake: {
      ...options.treeshake
    },
    input: Object.fromEntries(
      toArray<string>(options.input?.entries)
        .filter(Boolean)
        .map(entry => entryItem(entry))
        .concat([entryItem(appEntry, isBuild ? 'index' : undefined)])
    ),
    output: {
      strict: true,
      compact: true,
      sourcemapExcludeSources: isBuild,
      freeze: false,
      exports: 'named',
      minifyInternalExports: isBuild,
      ...options.output,
      file: undefined,
      sourcemap,
      entryFileNames: '[name].js',
      generatedCode: {
        constBindings: true,
        preset: 'es2015',
        ...(options.output as any)?.generatedCode
      },
      format: 'commonjs',
      dir: mainDir
    },
    plugins: [
      replaceDir(mainDir, rendererDir, sourcemap),
      ...prePlugins,
      mainSourceRoot && (hot({ disabled: isBuild, srcRoot: mainSourceRoot }) as any),
      definePlugin({
        replacements: {
          __DEV__: JSON.stringify(mode === 'development'),
          __PROD__: JSON.stringify(mode === 'production'),
          __RENDERER__: JSON.stringify(options.output?.rendererDir || DEFAULT_RENDERER_DIR),
          ...options.define
        },
        mode,
        sourcemap
      }),
      options.esbuild !== false
        ? esbuild(esbuildOptions)
        : [
            json({ preferConst: true, namedExports: true }),
            typescript({
              tsconfig: options.tsconfig,
              sourceMap: true,
              noEmit: !isBuild,
              ...(hasReflectMetadata && {
                emitDecoratorMetadata: true,
                experimentalDecorators: true
              })
            })
          ],
      assets({
        native: true,
        //@ts-ignore
        include: [defaultInclude, ...toArray(options.assetsInclude)],
        ...options.assetsOptions,
        target: 'node',
        base: undefined,
        root: root
      }),
      externalsPlugin({
        include: ['electron', 'electron-updater', ...toArray(options.external)],
        exclude: ['@xyh19/hot-module-cjs', 'lodash-es', ...toArray(options.noExternal)]
      }),
      nodeResolve({
        browser: false
      }),
      commonjs({
        ignoreTryCatch: false,
        ...options.commonjsOptions
      }),
      isBuild && analyzer(),
      ...postPlugins
    ].flat(),
    onwarn: warning => {
      log(warning, 'MAIN', 'yellow')
    }
  }
}
